package org.swift.commons.deployer.framework.axis2;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import javax.xml.namespace.QName;

import org.apache.axiom.soap.SOAP11Constants;
import org.apache.axiom.soap.SOAP12Constants;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.AxisBinding;
import org.apache.axis2.description.AxisBindingMessage;
import org.apache.axis2.description.AxisBindingOperation;
import org.apache.axis2.description.AxisEndpoint;
import org.apache.axis2.description.AxisMessage;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.InOutAxisOperation;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.description.java2wsdl.Java2WSDLConstants;
import org.apache.axis2.description.java2wsdl.TypeTable;
import org.apache.axis2.engine.MessageReceiver;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.transport.http.util.RESTUtil;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.utils.NamespaceMap;
import org.swift.commons.deployer.framework.axis2.AxisOperationExposure.HttpMethod;


@SuppressWarnings("unchecked")
public class AxisServiceFactory {
	private static Log log = LogFactory.getLog(AxisServiceFactory.class);

	public enum Protocol {
		SOAP11, SOAP12
	}

	private XmlSchema xmlSchema = null;

	private TypeTable typeTable;

	private Map<String, AxisOperation> httpLocationTable;

	private NamespaceMap namespaceMap;

	private ConfigurationContext configContext;

	private final String HTTP_TRANSPORT = "http";

	private final String HTTPS_TRANSPORT = "https";

	private AxisBinding soap11Binding;

	private AxisBinding httpBinding;

	private AxisOperationExposure operationExposure;

	private AxisBinding soap12Binding;

	@SuppressWarnings("unused")
	private String nsPrefix = "";

	private String targetNamespace = "";

	private String bindingPrefix = "";

	private String inMessageName = "inboundMessage";

	private String outMessageName = "outboundMessage";

	private String outputAction = "outputAction";

	public enum Direction {
		IN, OUT
	}

	/**
	 * @param prefix prefix to use for items created by this generator
	 * @param namespace the target namespace to use for services we create
	 * @param configContext the configuration context in which to push
	 *        operations into
	 * @param operationRepresentation
	 */
	public AxisServiceFactory(String prefix, String namespace,
			ConfigurationContext configContext,
			AxisOperationExposure operationExposure) {
		namespaceMap = new NamespaceMap();
		namespaceMap.put(prefix, namespace);
		nsPrefix = prefix;
		targetNamespace = namespace;
		this.operationExposure = operationExposure;
		this.configContext = configContext;
	}

	/**
	 * @param prefix prefix to use for items created by this generator
	 * @param namespace the target namespace to use for services we create
	 * @param configContext the configuration context in which to push
	 *        operations into
	 * @param operationRepresentation
	 * @param bindingPrefix the prefix to place on the names of bindings for
	 *        each binding operation
	 * 
	 */
	public AxisServiceFactory(String prefix, String namespace,
			ConfigurationContext configContext,
			AxisOperationExposure operationExposure, String bindingPrefix) {
		namespaceMap = new NamespaceMap();
		namespaceMap.put(prefix, namespace);
		nsPrefix = prefix;
		targetNamespace = namespace;
		this.operationExposure = operationExposure;
		this.configContext = configContext;
		this.bindingPrefix = bindingPrefix;
	}

	public AxisService createAxisService(String name,
			MessageReceiver messageReceiver, URL url, ClassLoader classLoader,
			Collection<String> operations) {
		AxisService axisService = null;
		try {
			axisService = new AxisService();

			XmlSchemaCollection collection = new XmlSchemaCollection();
			xmlSchema = new XmlSchema(targetNamespace, collection);
			xmlSchema.setNamespaceContext(namespaceMap);

			typeTable = new TypeTable();

			httpLocationTable = new TreeMap(new Comparator() {
				public int compare(Object o1, Object o2) {
					return (-1 * ((Comparable) o1).compareTo(o2));
				}
			});

			axisService.setTargetNamespace(xmlSchema.getTargetNamespace());
			axisService.setNamespaceMap(namespaceMap);
			axisService.setTypeTable(typeTable);
			axisService.setClassLoader(classLoader);
			axisService.setFileName(url);
			//axisService.addSchema(xmlSchema);
			axisService.setName(name);

			createDefaultHTTPBinding(bindingPrefix);
			createDefaultSOAP11Binding(bindingPrefix);
			createDefaultSOAP12Binding(bindingPrefix);

			createDefaultEndpoints(axisService);

			QName inMessageTypeName = typeTable
					.getComplexSchemaType(Object.class.getName());

			generateOperations(messageReceiver, axisService, inMessageTypeName,
					operations);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			throw new RuntimeException(e);
		}

		return axisService;
	}

	protected void generateOperations(MessageReceiver messageReceiver,
			AxisService axisService, QName inMessageTypeName,
			Collection<String> operationNames) throws AxisFault {
		for (String operationName : operationNames) {
			AxisOperation axisOp = createOperation(axisService, new QName(
					operationName), inMessageTypeName);
			axisOp.setMessageReceiver(messageReceiver);
			axisOp.setStyle(WSDLConstants.STYLE_DOC);
			/*
			 * axisOp.setDocumentation(operationName +
			 * " documentation (temporary code)");
			 */
			axisService.addOperation(axisOp);
			configContext.getAxisConfiguration().getPhasesInfo()
					.setOperationPhases(axisOp);
		}
	}

	/**
	 * Creates an axis operation that supports SOAP1.1,SOAP1.2, and REST
	 * communication.
	 * 
	 * @param name The name of the operation to create.
	 * @return
	 */
	protected AxisOperation createOperation(AxisService axisService,
			QName operationName, QName inMessageType) {
		AxisOperation operation = axisService.getOperation(operationName);
		if (operation != null) {
			throw new RuntimeException(
					String
							.format(
									"There is a conflict in operation names.  Operation [%s] already exists for this service.",
									operationName.getLocalPart()));
		}
		AxisOperation axisOperation = createBaseOperation(operationName);
		Collection<AxisBindingOperation> bindingOperations = createBindingOperations(
				axisService.getName(), axisOperation);
		for (Direction direction : Direction.values()) {
			String axisDirection = getAxisDirection(direction);
			AxisMessage message = axisOperation.getMessage(axisDirection);
			for (AxisBindingOperation bindingOperation : bindingOperations) {
				createAxisBindingMessage(bindingOperation, message,
						axisDirection);
			}
			if (Direction.IN.equals(direction)) {
				message.setElementQName(inMessageType);
				message.setName(inMessageName);
			} else {
				message.setElementQName(inMessageType);
				message.setName(outMessageName);
			}
		}
		return axisOperation;
	}

	private String getAxisDirection(Direction dir) {
		switch (dir) {
			case OUT:
				return WSDLConstants.MESSAGE_LABEL_OUT_VALUE;
			case IN:
			default:
				return WSDLConstants.MESSAGE_LABEL_IN_VALUE;
		}
	}

	protected AxisOperation createBaseOperation(QName operationName) {
		return new InOutAxisOperation(operationName);
	}

	protected Collection<AxisBindingOperation> createBindingOperations(
			String serviceName, AxisOperation axisOperation) {
		axisOperation.setOutputAction(outputAction);
		Collection<AxisBindingOperation> operations = new ArrayList<AxisBindingOperation>();
		// The following loop creates the rest bindings for each operation
		for (Map.Entry<HttpMethod, String> httpEntry : operationExposure
				.getSupportedRestMethods(axisOperation.getName()).entrySet()) {
			String httpLocation = httpEntry.getValue();
			String method = convertToAxisHttpBinding(httpEntry.getKey());
			String httpLocationString = httpLocation;
			if (httpLocation != null && httpLocation.trim().length() > 0) {
				httpLocationString = RESTUtil.getConstantFromHTTPLocation(
						httpLocation, method);
			} else {
				httpLocationString = method + serviceName;
			}
			operations.add(createDefaultHTTPBindingOperation(axisOperation,
					httpLocation, method, false));
			httpLocationTable.put(httpLocationString, axisOperation);
		}
		String inputAction = operationExposure.getSoapInputAction(serviceName,
				axisOperation.getName());
		// This will set up the SOAP operations for each operation.
		if (inputAction != null && inputAction.trim().length() > 0) {
			// QName currentName = axisOperation.getName();
			axisOperation.setSoapAction(inputAction);
			// axisOperation.setName(new QName(currentName.getNamespaceURI(),
			// inputAction, currentName.getPrefix()));
			for (Protocol protocol : Protocol.values()) {
				switch (protocol) {
					case SOAP11:
						operations.add(createDefaultSOAP11BindingOperation(
								axisOperation, inputAction));
					case SOAP12:
						operations.add(createDefaultSOAP12BindingOperation(
								axisOperation, inputAction));
						break;
				}
			}
		}
		return operations;
	}

	/**
	 * Creates a set of default endpoints for this service
	 * 
	 * @param axisService The AxisService that the endpoints are created for
	 */
	private void createDefaultEndpoints(AxisService axisService) {
		HashMap transportsIn = configContext.getAxisConfiguration()
				.getTransportsIn();
		Iterator iterator = transportsIn.values().iterator();
		while (iterator.hasNext()) {
			// Used to indicate whather a HTTPEndpoint is needed. Http endpoint
			// is needed only
			// for http and https transports
			boolean needHttp = false;

			// The prefix is used to genarate endpoint names
			String prefix = "";
			TransportInDescription transportIn = (TransportInDescription) iterator
					.next();
			String transportInName = transportIn.getName();
			if (HTTP_TRANSPORT.equalsIgnoreCase(transportInName)) {
				needHttp = true;
			} else if (HTTPS_TRANSPORT.equalsIgnoreCase(transportInName)) {
				needHttp = true;
				prefix = WSDL2Constants.DEFAULT_HTTPS_PREFIX;
			} else if (transportInName != null) {
				prefix = transportInName.toUpperCase();
			}

			if (!org.apache.axis2.Constants.TRANSPORT_MAIL
					.equalsIgnoreCase(transportInName)) {
				// Creates a default SOAP 1.1 endpoint
				AxisEndpoint soap11Endpoint = new AxisEndpoint();
				String soap11EndpointName = prefix
						+ WSDL2Constants.DEFAULT_SOAP11_ENDPOINT_NAME;
				soap11Endpoint.setName(soap11EndpointName);
				soap11Endpoint.setBinding(soap11Binding);
				soap11Endpoint.setParent(axisService);
				soap11Endpoint.setTransportInDescription(transportInName);
				axisService.addEndpoint(soap11EndpointName, soap11Endpoint);
			}

			// Creates a default SOAP 1.2 endpoint
			AxisEndpoint soap12Endpoint = new AxisEndpoint();
			String soap12EndpointName = prefix
					+ WSDL2Constants.DEFAULT_SOAP12_ENDPOINT_NAME;
			soap12Endpoint.setName(soap12EndpointName);
			soap12Endpoint.setBinding(soap12Binding);
			soap12Endpoint.setParent(axisService);
			soap12Endpoint.setTransportInDescription(transportInName);
			axisService.addEndpoint(soap12EndpointName, soap12Endpoint);

			// Creates a HTTP endpoint if its http or https transport is used
			if (needHttp) {
				AxisEndpoint httpEndpoint = new AxisEndpoint();
				String httpEndpointName = prefix
						+ WSDL2Constants.DEFAULT_HTTP_ENDPOINT_NAME;
				httpEndpoint.setName(httpEndpointName);
				httpEndpoint.setBinding(httpBinding);
				httpEndpoint.setParent(axisService);
				httpEndpoint.setTransportInDescription(transportInName);
				axisService.addEndpoint(httpEndpointName, httpEndpoint);
			}
		}
	}

	/**
	 * Creates AxisBindingOperation and populates it with HTTP properties
	 * 
	 * @param axisOp The AxisOperation corresponding to this bindingOperation
	 * @param httpLocation The httpLocation annotation for this operation
	 * @param httpMethod The httpMethod annotation for this operation
	 * @return AxisBindingOperation having sdefault HTTP values
	 */
	private AxisBindingOperation createDefaultHTTPBindingOperation(
			AxisOperation axisOp, String httpLocation, String httpMethod,
			boolean ignoreUncited) {
		AxisBindingOperation httpBindingOperation = new AxisBindingOperation();
		httpBindingOperation.setAxisOperation(axisOp);
		httpBindingOperation.setName(axisOp.getName());
		httpBindingOperation.setParent(httpBinding);
		httpBindingOperation.setProperty(WSDL2Constants.ATTR_WHTTP_METHOD,
				httpMethod);
		httpBindingOperation.setProperty(
				WSDL2Constants.ATTR_WHTTP_IGNORE_UNCITED, ignoreUncited);
		httpBinding.addChild(httpBindingOperation.getName(),
				httpBindingOperation);
		setHttpLocation(httpBindingOperation, httpLocation);
		return httpBindingOperation;
	}

	/**
	 * Creates AxisBindingOperation and populates it with SOAP 1.1 properties
	 * 
	 * @param axisOp The AxisOperation corresponding to this bindingOperation
	 * @param httpLocation The httpLocation annotation for this operation
	 * @param inputAction The input action for this operation
	 * @return AxisBindingOperation having sdefault SOAP 1.1 values
	 */
	private AxisBindingOperation createDefaultSOAP11BindingOperation(
			AxisOperation axisOp, String inputAction) {
		AxisBindingOperation soap11BindingOperation = new AxisBindingOperation();
		soap11BindingOperation.setAxisOperation(axisOp);
		soap11BindingOperation.setName(axisOp.getName());
		soap11BindingOperation.setParent(soap11Binding);
		soap11Binding.addChild(soap11BindingOperation.getName(),
				soap11BindingOperation);
		soap11BindingOperation.setProperty(WSDL2Constants.ATTR_WSOAP_ACTION,
				inputAction);
		return soap11BindingOperation;
	}

	/**
	 * Creates AxisBindingOperation and populates it with SOAP 1.2 properties
	 * 
	 * @param axisOp The AxisOperation corresponding to this bindingOperation
	 * @param httpLocation The httpLocation annotation for this operation
	 * @param inputAction The input action for this operation
	 * @return AxisBindingOperation having sdefault SOAP 1.2 values
	 */
	private AxisBindingOperation createDefaultSOAP12BindingOperation(
			AxisOperation axisOp, String inputAction) {
		AxisBindingOperation soap12BindingOperation = new AxisBindingOperation();
		soap12BindingOperation.setAxisOperation(axisOp);
		soap12BindingOperation.setName(axisOp.getName());
		soap12BindingOperation.setParent(soap12Binding);
		soap12Binding.addChild(soap12BindingOperation.getName(),
				soap12BindingOperation);
		soap12BindingOperation.setProperty(WSDL2Constants.ATTR_WSOAP_ACTION,
				inputAction);
		return soap12BindingOperation;
	}

	private void setHttpLocation(AxisBindingOperation operation, String location) {
		if (location != null && location.trim().length() > 0) {
			operation.setProperty(WSDL2Constants.ATTR_WHTTP_LOCATION, location);
		}
	}

	/**
	 * Creates a AxisBinding and populates it with default HTTP properties
	 * 
	 * @param name The name of the service
	 * @param interfaceName The interface name
	 */
	private void createDefaultHTTPBinding(String name) {
		httpBinding = new AxisBinding();
		httpBinding.setName(new QName(name
				+ WSDL2Constants.DEFAULT_HTTP_ENDPOINT_NAME));
		httpBinding.setType(WSDL2Constants.URI_WSDL2_HTTP);
		httpBinding.setProperty(WSDL2Constants.ATTR_WHTTP_METHOD,
				HTTPConstants.HTTP_METHOD_GET);
		httpBinding.setProperty(WSDL2Constants.HTTP_LOCATION_TABLE,
				httpLocationTable);
	}

	/**
	 * Creates a AxisBinding and populates it with default SOAP 1.1 properties
	 * 
	 * @param name The name of the service
	 * @param interfaceName The interface name
	 */
	private void createDefaultSOAP11Binding(String name) {
		soap11Binding = new AxisBinding();
		soap11Binding.setName(new QName(name
				+ WSDL2Constants.DEFAULT_SOAP11_ENDPOINT_NAME));
		soap11Binding.setType(WSDL2Constants.URI_WSDL2_SOAP);
		soap11Binding.setProperty(WSDL2Constants.ATTR_WSOAP_PROTOCOL,
				WSDL2Constants.HTTP_PROTOCAL);
		soap11Binding.setProperty(WSDL2Constants.ATTR_WSOAP_VERSION,
				SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI);
		soap11Binding.setProperty(WSDL2Constants.HTTP_LOCATION_TABLE,
				httpLocationTable);
	}

	/**
	 * Creates a AxisBinding and populates it with default SOAP 1.2 properties
	 * 
	 * @param name The name of the service
	 * @param interfaceName The interface name
	 */
	private void createDefaultSOAP12Binding(String name) {
		soap12Binding = new AxisBinding();
		soap12Binding.setName(new QName(name
				+ Java2WSDLConstants.SOAP12BINDING_NAME_SUFFIX));
		soap12Binding.setType(WSDL2Constants.URI_WSDL2_SOAP);
		soap12Binding.setProperty(WSDL2Constants.ATTR_WSOAP_PROTOCOL,
				WSDL2Constants.HTTP_PROTOCAL);
		soap12Binding.setProperty(WSDL2Constants.ATTR_WSOAP_VERSION,
				SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI);
		soap12Binding.setProperty(WSDL2Constants.HTTP_LOCATION_TABLE,
				httpLocationTable);
	}

	private void createAxisBindingMessage(
			AxisBindingOperation bindingOperation, AxisMessage message,
			String direction) {
		AxisBindingMessage inBindingMessage = new AxisBindingMessage();
		inBindingMessage.setName(message.getName());
		inBindingMessage.setAxisMessage(message);
		inBindingMessage.setParent(bindingOperation);
		bindingOperation.addChild(direction, inBindingMessage);
	}

	private String convertToAxisHttpBinding(HttpMethod httpMethod) {
		switch (httpMethod) {
			case DELETE:
				return HTTPConstants.HEADER_DELETE;
			case POST:
				return HTTPConstants.HEADER_POST;
			case GET:
				return HTTPConstants.HEADER_GET;
			case PUT:
				return HTTPConstants.HEADER_PUT;
		}
		return HTTPConstants.HEADER_GET;
	}
}